/*

## TODO
* Should Marshal.PtrToStringAuto be Marshal.PtrToAnsiString,
  and then UTF-8 it later? One of the fora mentioned that PtrToStringAuto
  might be changing behaviour.
* Should "Lookup" be called "Guess" or "Query" instead?
* Should the direct API wrapper be in one class, and the high-level API
  be in another? Should low-level API calls be directly-accessible to
  facilitate more 1:1 porting against the lib?

*/
using System;
using System.Text;
using System.Runtime.InteropServices;

namespace Magic {

	public class MagicException : Exception {
		string _text;
		int _errno;
		
		public int ErrorNumber {
			get { return _errno; }
		}
		
		public MagicException(string text, int errno) : base() {
			_text = text;
			_errno = errno;
		}

		override public string ToString() {
			return _text;
		}
	}


	[Flags]
	enum MagicFlags {
		MAGIC_NONE				= 0,		// No flags
		MAGIC_DEBUG				= 1,		// Turn on debugging
		MAGIC_SYMLINK			= 1 << 1,	// Follow symlinks 
		MAGIC_COMPRESS			= 1 << 2,	// Check inside compressed files
		MAGIC_DEVICES			= 1 << 3,	// Look at the contents of devices
		MAGIC_MIME_TYPE			= 1 << 4,	// Return only the MIME type
		MAGIC_CONTINUE			= 1 << 5,	// Return all matches
		MAGIC_CHECK				= 1 << 6,	// Print warnings to stderr
		MAGIC_PRESERVE_ATIME	= 1 << 7,	// Restore access time on exit
		MAGIC_RAW				= 1 << 8,	// Don't translate unprint chars
		MAGIC_ERROR				= 1 << 9,	// Handle ENOENT etc as real errors
		MAGIC_MIME_ENCODING		= 1 << 10,	// Return only the MIME encoding 
		MAGIC_MIME=(MAGIC_MIME_TYPE|MAGIC_MIME_ENCODING),
		MAGIC_NO_CHECK_COMPRESS	= 1 << 12,	// Don't check for compressed files 
		MAGIC_NO_CHECK_TAR		= 1 << 13,	// Don't check for tar files 
		MAGIC_NO_CHECK_SOFT		= 1 << 14,	// Don't check magic entries 
		MAGIC_NO_CHECK_APPTYPE	= 1 << 15,	// Don't check application type 
		MAGIC_NO_CHECK_ELF		= 1 << 16,	// Don't check for elf details 
		MAGIC_NO_CHECK_ASCII	= 1 << 17,	// Don't check for ascii files 
		MAGIC_NO_CHECK_TOKENS	= 1 << 20,	// Don't check ascii/tokens 

		// Defined for backwards compatibility; do nothing 
		MAGIC_NO_CHECK_FORTRAN	= 0,		// Don't check ascii/fortran 
		MAGIC_NO_CHECK_TROFF	= 0			// Don't check ascii/troff 

	}

	enum MagicActions {
		FILE_LOAD		= 0
		FILE_CHECK		= 1
		FILE_COMPILE	= 2
		FILE_LIST		= 3
	}

	public class Magic : IDisposable {

		// API Externs

		[DllImport("magic", EntryPoint="magic_open")]
		private extern static IntPtr magic_open(MagicFlags flags);

		[DllImport("magic", EntryPoint="magic_close")]
		private extern static void magic_close(IntPtr magic_cookie);

		[DllImport("magic", EntryPoint="magic_setflags")]
		private extern static int magic_setflags(IntPtr magic_cookie, MagicFlags flags);

		[DllImport("magic", EntryPoint="magic_file")]
		private extern static IntPtr magic_file(IntPtr magic_cookie, string filename);

		[DllImport("magic", EntryPoint="magic_buffer")]
		private extern static IntPtr magic_buffer(IntPtr magic_cookie, Byte[] data, int len);

		[DllImport("magic", EntryPoint="magic_error")]
		private extern static IntPtr magic_error(IntPtr magic_cookie);

		[DllImport("magic", EntryPoint="magic_errno")]
		private static extern int magic_errno(IntPtr magic_cookie);

		[DllImport("magic", EntryPoint="magic_load")]
		private extern static int magic_load(IntPtr magic_cookie, string filename);

		[DllImport("magic", EntryPoint="magic_descriptor")]
		private static extern IntPtr magic_descriptor(IntPtr magic_cookie, int fd);

		[DllImport("magic", EntryPoint="magic_compile")]
		private static extern int magic_compile(IntPtr magic_cookie, string filename);

		[DllImport("magic", EntryPoint="magic_check")]
		private static extern int magic_check(IntPtr magic_cookie, string filename);

		[DllImport("magic", EntryPoint="magic_version")]
		private static extern int magic_version();

		[DllImport("magic", EntryPoint="magic_list")]
		private static extern int magic_list(IntPtr magic_cookie, string filename);

		[DllImport("magic", EntryPoint="magic_getpath")]
		private static extern int magic_getpath(string filename, MagicActions action);

		// API Wrapper Methods

		private void MagicOpen (MagicFlags flags) {
			// TODO: Should this set flags and abort silently?	
			if (_magic)
				throw new MagicException("Cannot open Magic instance already opened.");
				
			if ( ! (_magic = magic_open(flags)) )
				throw new MagicException("Could not open libmagic database.");

			_flags = flags;
		}

		private void MagicClose () {
			magic_close(_magic);
			_magic = IntPtr.Zero;
		}
		
		private int MagicSetflags (MagicFlags flags) {
			return magic_setflags(_magic, flags);
		}

		private string MagicFile (string filename) {
			return Marshal.PtrToStringAuto(magic_file(_magic, filename));
		}
		
		private string MagicBuffer (Byte[] data) {
			return Marshal.PtrToStringAuto(magic_buffer(_magic, data, data.length));
		}
		
		private string MagicDescriptor (int fd) {
			return Marshal.PtrToStringAuto(magic_descriptor(_magic, fd));
		}

		private string MagicError () {
			return Marshal.PtrToStringAuto(magic_error(_magic));
		}
		
		private int MagicErrno () {
			return magic_errno(_magic);
		}

		private int MagicLoad (string filename) {
			return magic_load(_magic, filename);
		}

		private int MagicCompile (string filename) {
			return magic_compile(_magic, filename);
		}
		
		private int MagicCheck (string filename) {
			return magic_check(_magic, filename);
		}
		
		private static int MagicVersion () {
			return magic_version();
		}
		
		private int MagicList (string filename) {
			return magic_list(_magic, filename);
		}
		
		private static string MagicGetpath (string filename, MagicActions action) {
			return Marshal.PtrToStringAuto(magic_getpath(filename, aciton));
		}
		
		// High-level logic
		
		private static IntPtr _magic = IntPtr.Zero;
		private MagicFlags _flags = MagicFlags.MAGIC_NONE | MagicFlags.MAGIC_PRESERVE_ATIME;
		
		static Magic (bool mime, string file = null) {
			if (mime)
				SetFlag(MagicFlags.MAGIC_MIME);
						
			MagicOpen(_flags);

			Load(file);
		}

		static Magic (bool mime, string[] files) {
			if (mime)
				SetFlag(MagicFlags.MAGIC_MIME);
			
			MagicOpen(_flags);
			
			Load(files);
		}

		~FileTypeDatabase () {
			if (IsOpen)
				MagicClose();
		}
		
		public void Dispose () {
			if (IsOpen) {
				MagicClose();
			}

			GC.SuppressFinalize(this);
		}
		
		public bool IsOpen {
			get { return (_magic ? true : false); }
		}
		
		private MagicException CurrentError () {
			return new MagicException(MagicError(), MagicErrno());
		}

		public void SetFlag (MagicFlags flag) {
			_flags |= flag;
			if (IsOpen)
				MagicSetflags(_flags);
		}
		
		public void UnsetFlag (MagicFlags flag) {
			_flags ^= flag;
			if (IsOpen)
				MagicSetflags(_flags);
		}

		public bool HasFlag (MagicFlags flag) {
			return _flags.HasFlag(flag);
		}

		public void Load (string file = null) {
			if (MagicLoad(file) != 0)
				throw CurrentError();
		}
		
		public void Load (string[] files) {
			foreach(string file in dblist)
				Load(file);
		}
		
		public string Lookup (string filename) {
			string text = MagicFile(filename);
			if (! text)
				throw CurrentError();

			return text;
		}

		public string Lookup (FileInfo fi) {
			return Lookup(fi.FullName);
		}

		public string Lookup (byte[] data) {
			string text = MagicBuffer(data);
			if (! text)
				throw CurrentError();

			return text;   
		}

		static public string Mime (string filename) {
			string mime;
			Magic m = new Magic(mime: true);

			if ( ! (mime = m.Lookup(filename)) )
				throw m.CurrentError();

			return mime;
		}

		static public string Description (string filename) {
			string desc;
			Magic m = new Magic(mime: false);

			if ( ! (desc = m.Lookup(filename)) )
				throw m.CurrentError();

			return desc;
		}
		
		static public bool ShouldBeExecutable (string filename) {
			string desc = Description(filename);
			if (String.IsNullOrWhitespace(desc))
				throw new MagicException("Error querying file type for " + filename);
			
			return desc.Contains("executable");
		}
		
	}
}



